#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <assert.h>
#include "renderer.h"

#include <sokol_gfx.h>
#include <sokol_log.h>
#include <sokol_glue.h>
#include "pacman_sapp.h"

#define VERTEX_UV_BUFFER 0
#define PALETTE_BUFFER 1
#define INDEX_BUFFER 2

#define SCREEN_PADDING_X 5
#define SCREEN_PADDING_Y 10

static void setup_buffers(
    sg_buffer buffers[3],
    const float* vertex_uv_data,     const float* palette_data,     const uint16_t* index_data,
    const size_t vertex_uv_buf_size, const size_t palette_buf_size, const size_t index_buf_size
) {
    // vertex buffer
    buffers[VERTEX_UV_BUFFER] = sg_make_buffer(&(sg_buffer_desc){
        .usage = SG_USAGE_STREAM,
        .type = SG_BUFFERTYPE_VERTEXBUFFER,
        .size = vertex_uv_buf_size
    });
    //sg_update_buffer(buffers[VERTEX_UV_BUFFER], &(sg_range) { .ptr = vertex_uv_data, .size = vertex_uv_buf_size });

    // palette buffer
    buffers[PALETTE_BUFFER] = sg_make_buffer(&(sg_buffer_desc){
        .usage = SG_USAGE_STREAM,
        .type = SG_BUFFERTYPE_VERTEXBUFFER,
        .size = palette_buf_size
    });
    //sg_update_buffer(buffers[PALETTE_BUFFER], &(sg_range) { .ptr = palette_data, .size = palette_buf_size });

    // index buffer
    buffers[INDEX_BUFFER] = sg_make_buffer(&(sg_buffer_desc){
        .data = { .ptr = index_data, .size = index_buf_size },
        .usage = SG_USAGE_IMMUTABLE,
        .type = SG_BUFFERTYPE_INDEXBUFFER
    });
}

int game_renderer_init(GameRenderer* gfx_data, GameInterface* pacman) {
    gfx_data->environment = sglue_environment();

    sg_setup(&(sg_desc){
        .environment = gfx_data->environment,
        .logger.func = slog_func
    });

    int screen_width = pacman->display_info.screen_width + SCREEN_PADDING_X * 2;
    int screen_height = pacman->display_info.screen_height + SCREEN_PADDING_Y * 2;

    // initialize render buffer
    {
        gfx_data->fbo_color = sg_make_image(&(sg_image_desc){
            .render_target = true,
            .width = screen_width,
            .height = screen_height,
            .pixel_format = SG_PIXELFORMAT_RGBA8,
            .sample_count = 1,
        });

        gfx_data->fbo_depth = sg_make_image(&(sg_image_desc){
            .render_target = true,
            .width = screen_width,
            .height = screen_height,
            .pixel_format = gfx_data->environment.defaults.depth_format,
            .sample_count = 1
        });

        gfx_data->fbo = sg_make_attachments(&(sg_attachments_desc){
            .colors[0].image = gfx_data->fbo_color,
            .depth_stencil.image = gfx_data->fbo_depth
        });
    }

    // load display rect
    {
        float width = screen_width * 2.0f;
        float height = screen_height * 2.0f;
        const float data[] = { // vec2 pos + vec2 uv data
            0.0f, 0.0f,
            0.0f, 1.0f,

            0.0f, height,
            0.0f, 0.0f,

            width, height,
            1.0f, 0.0f,

            width, 0.0f,
            1.0f, 1.0f
        };

        const uint16_t indices[] = { 0, 1, 2, 2, 3, 0};

        // create pos/uv buffer
        gfx_data->display_vbuffers[0] = sg_make_buffer(&(sg_buffer_desc){
            .data = SG_RANGE(data),
            .usage = SG_USAGE_IMMUTABLE,
            .type = SG_BUFFERTYPE_VERTEXBUFFER
        });

        // create index buffer
        gfx_data->display_vbuffers[1] = sg_make_buffer(&(sg_buffer_desc){
            .data = SG_RANGE(indices),
            .usage = SG_USAGE_IMMUTABLE,
            .type = SG_BUFFERTYPE_INDEXBUFFER
        });
    }

    // initialize shaders
    {
        gfx_data->shader = sg_make_shader(palette_shader_desc(sg_query_backend()));
        gfx_data->display_shader = sg_make_shader(display_shader_desc(sg_query_backend()));
    }

    // load character texture atlas
    // each character is an 8x8 image
    // the image itself is a 16x16 grid of tiles

    // each byte stores a vertical strip of 4 pixels as a bitplane
    // the strips are ordered left-to-right, bottom-to-top
    // there are 8 strips horizontally and 2 strips vertically
    {
        const int tex_width = 128;
        const int tex_height = 128;

        uint8_t* pixels = (uint8_t*) calloc(tex_width * tex_height, sizeof(uint8_t));
        const uint8_t* src_pixels = pacman->display_info.characterGraphics;

        int i = 0;
        for (int tileIndex = 0; tileIndex < 256; tileIndex++) {
            int y = (tileIndex / 16) * 8;
            int x = (tileIndex % 16) * 8;

            for (int row = 0; row < 8; row += 4) {
                for (int col = 7; col >= 0; col--) {
                    assert((y + row + 3) * tex_width + x + col < tex_width * tex_height);
                    assert(i < 4096); // CHARACTER_DATA_LEN

                    pixels[(y + row + 0) * tex_width + x + col] = (((src_pixels[i] >> 4) & 1) << 1) | ((src_pixels[i] >> 0) & 1);
                    pixels[(y + row + 1) * tex_width + x + col] = (((src_pixels[i] >> 5) & 1) << 1) | ((src_pixels[i] >> 1) & 1);
                    pixels[(y + row + 2) * tex_width + x + col] = (((src_pixels[i] >> 6) & 1) << 1) | ((src_pixels[i] >> 2) & 1);
                    pixels[(y + row + 3) * tex_width + x + col] = (((src_pixels[i] >> 7) & 1) << 1) | ((src_pixels[i] >> 3) & 1);
                    i++;
                } 
            }
        }

        gfx_data->character_texture = sg_make_image(&(sg_image_desc){
            .label = "character-texture",
            .width = tex_width,
            .height = tex_height,
            .pixel_format = SG_PIXELFORMAT_R8,
            .data.subimage[0][0] = { .ptr = pixels, .size = tex_width * tex_height * sizeof(uint8_t) }
        });

        free(pixels);
    }

    // load sprite texture atlas
    // each sprite is a 16x16 image
    // the format is similar to the one for the character data, but
    // there are a few differences.
    // each sprite is split into eight 8x4 chunks, like how a character
    // is split into two 8x4 chunks. the ordering of these chunks in the
    // data is weird which is why there is a table.
    {
        const int tex_width = 128;
        const int tex_height = 128;
        const int ordering[] = { 5, 1, 6, 2, 7, 3, 4, 0 };

        uint8_t* pixels = (uint8_t*) calloc(tex_width * tex_height, sizeof(uint8_t));
        const uint8_t* src_pixels = pacman->display_info.spriteGraphics;

        int i = 0;
        for (int tileIndex = 0; tileIndex < 64; tileIndex++) {
            int y = (tileIndex / 8) * 16;
            int x = (tileIndex % 8) * 16;
            int j = 0;

            uint8_t byte;
            for (int row = 12; row >= 0; row -= 4) {
                for (int c = 0; c < 2; c++) {
                    int k = i + ordering[j] * 8;
                    for (int col = 7; col >= 0; col--) {
                        assert((y + row + 3) * tex_width + x + col + c * 8);
                        assert(k < 4096); // SPRITE_DATA_LEN

                        byte = src_pixels[k];
                        pixels[(y + row + 0) * tex_width + x + col + c * 8] = (((byte >> 4) & 1) << 1) | ((byte >> 0) & 1);
                        pixels[(y + row + 1) * tex_width + x + col + c * 8] = (((byte >> 5) & 1) << 1) | ((byte >> 1) & 1);
                        pixels[(y + row + 2) * tex_width + x + col + c * 8] = (((byte >> 6) & 1) << 1) | ((byte >> 2) & 1);
                        pixels[(y + row + 3) * tex_width + x + col + c * 8] = (((byte >> 7) & 1) << 1) | ((byte >> 3) & 1);
                        k++;
                    }
                    j++;
                }
            }

            i += 64;
        }

        gfx_data->sprite_texture = sg_make_image(&(sg_image_desc){
            .label = "sprite-texture",
            .width = tex_width,
            .height = tex_height,
            .pixel_format = SG_PIXELFORMAT_R8,
            .data.subimage[0][0] = { .ptr = pixels, .size = tex_width * tex_height * sizeof(uint8_t) }
        });

        free(pixels);
    }

    // create tile draw buffer
    {
        const size_t vertex_uv_buf_size = TILE_WIDTH * TILE_HEIGHT * sizeof(float) * 16;
        const size_t palette_buf_size = TILE_WIDTH * TILE_HEIGHT * sizeof(float) * 4;
        const size_t index_buf_size = TILE_WIDTH * TILE_HEIGHT * sizeof(uint16_t) * 6;

        float* vertex_uv_data = (float*) malloc(vertex_uv_buf_size);
        float* palette_data = (float*) malloc(palette_buf_size);
        uint16_t* index_data = (uint16_t*) malloc(index_buf_size);

        // zero-initialize palette data
        memset(palette_data, 0, palette_buf_size);

        // fill in vertex/uv data and index data
        // vertex order starts from the top-left and goes counterclockwise around quad
        int i0 = 0;
        int i1 = 0;
        int i2 = 0;
        for (int x = 0; x < TILE_WIDTH; x++) {
            for (int y = 0; y < TILE_HEIGHT; y++) {
                // fill vertex/uv data
                vertex_uv_data[i0++] = (float) x;
                vertex_uv_data[i0++] = (float) y;
                vertex_uv_data[i0++] = 0.0f;
                vertex_uv_data[i0++] = 0.0f;

                vertex_uv_data[i0++] = (float) x;
                vertex_uv_data[i0++] = (float) y+1;
                vertex_uv_data[i0++] = 0.0f;
                vertex_uv_data[i0++] = 0.0f;

                vertex_uv_data[i0++] = (float) x+1;
                vertex_uv_data[i0++] = (float) y+1;
                vertex_uv_data[i0++] = 0.0f;
                vertex_uv_data[i0++] = 0.0f;

                vertex_uv_data[i0++] = (float) x+1;
                vertex_uv_data[i0++] = (float) y;
                vertex_uv_data[i0++] = 0.0f;
                vertex_uv_data[i0++] = 0.0f;
                
                // fill index buffer data
                index_data[i1++] = i2 + 0;
                index_data[i1++] = i2 + 1;
                index_data[i1++] = i2 + 2;

                index_data[i1++] = i2 + 2;
                index_data[i1++] = i2 + 3;
                index_data[i1++] = i2 + 0;
                i2 += 4;

                assert(i0 <= TILE_WIDTH * TILE_HEIGHT * 16);
                assert(i1 <= TILE_WIDTH * TILE_HEIGHT * 6);
                assert(i2 <= TILE_WIDTH * TILE_HEIGHT * 4);
            }
        }
        
        // create gpu buffers
        setup_buffers(
            gfx_data->tile_buffers,
            vertex_uv_data, palette_data, index_data,
            vertex_uv_buf_size, palette_buf_size, index_buf_size
        );

        free(index_data);

        gfx_data->tile_buf = vertex_uv_data;
        gfx_data->tile_palette_buf = palette_data;
        gfx_data->tile_buf_size = vertex_uv_buf_size;
        gfx_data->tile_palette_buf_size = palette_buf_size;
    }

    // create sprite draw buffers
    {
        const size_t vertex_uv_buf_size = MAX_SPRITES * sizeof(float) * 16;
        const size_t palette_buf_size = MAX_SPRITES * sizeof(float) * 4;
        const size_t index_buf_size = MAX_SPRITES * sizeof(uint16_t) * 6;

        float* vertex_uv_data = (float*) malloc(vertex_uv_buf_size);
        float* palette_data = (float*) malloc(palette_buf_size);
        uint16_t* index_data = (uint16_t*) malloc(index_buf_size);

        // zero-initialize palette and vertex/uv data
        // they will get filled in later
        memset(palette_data, 0, palette_buf_size);
        memset(vertex_uv_data, 0, vertex_uv_buf_size);

        // fill in index data
        // vertex order starts from the top-left and goes counterclockwise around quad
        int i = 0;
        for (int j = 0; j < MAX_SPRITES; j++) {
            index_data[i++] = j * 4 + 0;
            index_data[i++] = j * 4 + 1;
            index_data[i++] = j * 4 + 2;

            index_data[i++] = j * 4 + 2;
            index_data[i++] = j * 4 + 3;
            index_data[i++] = j * 4 + 0;

            assert(i <= MAX_SPRITES * 6);
            assert(j * 4 + 3 < MAX_SPRITES * 4);
        }
        
        // create gpu buffers
        setup_buffers(
            gfx_data->sprite_buffers,
            vertex_uv_data, palette_data, index_data,
            vertex_uv_buf_size, palette_buf_size, index_buf_size
        );

        free(index_data);

        gfx_data->sprite_buf = vertex_uv_data;
        gfx_data->sprite_palette_buf = palette_data;
        gfx_data->sprite_buf_size = vertex_uv_buf_size;
        gfx_data->sprite_palette_buf_size = palette_buf_size;
    }

    // initialize palette
    {
        const Color* colors = pacman->display_info.colors;
        uint8_t palette_data[64 * 4 * 4];

        int j = 0;
        for (int i = 0; i < 64; i++) {
            const Palette palette = pacman->display_info.palettes[i];

            for (int col = 0; col < 4; col++) {
                palette_data[j++] = colors[palette.colors[col]].r;
                palette_data[j++] = colors[palette.colors[col]].g;
                palette_data[j++] = colors[palette.colors[col]].b;
                
                // all black is interpreted as clear
                if (colors[palette.colors[col]].r == 0 && colors[palette.colors[col]].g == 0 && colors[palette.colors[col]].b == 0) {
                    palette_data[j++] = 0;
                } else {
                    palette_data[j++] = 255;
                }
            }
        }

        // upload palette data as texture
        gfx_data->palette_texture = sg_make_image(&(sg_image_desc){
            .label = "palette",
            .width = 4,
            .height = 64,
            .pixel_format = SG_PIXELFORMAT_RGBA8,
            .data.subimage[0][0] = SG_RANGE(palette_data)
        });
    }

    // setup sampler
    gfx_data->sampler = sg_make_sampler(&(sg_sampler_desc){
        .mag_filter = SG_FILTER_NEAREST,
        .min_filter = SG_FILTER_NEAREST,
        .wrap_u = SG_WRAP_CLAMP_TO_EDGE,
        .wrap_v = SG_WRAP_CLAMP_TO_EDGE,
    });

    // setup pipelines
    {
        gfx_data->game_pip = sg_make_pipeline(&(sg_pipeline_desc){
            .layout = {
                .buffers[0].stride = sizeof(float) * 4,
                .buffers[1].stride = sizeof(int),
                .attrs = {
                    [ATTR_palette_vs_v_pos] =
                        { .format = SG_VERTEXFORMAT_FLOAT2, .buffer_index = 0 },
                    [ATTR_palette_vs_v_uv] =
                        { .format = SG_VERTEXFORMAT_FLOAT2, .buffer_index = 0, .offset = sizeof(float) * 2 },
                    [ATTR_palette_vs_v_palette] = {
                        .format = SG_VERTEXFORMAT_FLOAT,
                        .buffer_index = 1
                    }
                }
            },
            .colors[0].blend = {
                .enabled = true,
                .op_rgb = SG_BLENDOP_ADD,
                .op_alpha = SG_BLENDOP_ADD,
                .src_factor_rgb = SG_BLENDFACTOR_SRC_ALPHA,
                .dst_factor_rgb = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA,
                .src_factor_alpha = SG_BLENDFACTOR_ONE,
                .dst_factor_alpha = SG_BLENDFACTOR_ZERO,
            },
            .colors[0].pixel_format = SG_PIXELFORMAT_RGBA8,
            .shader = gfx_data->shader,
            .index_type = SG_INDEXTYPE_UINT16,
            .label = "game-pipeline"
        });

        gfx_data->window_pip = sg_make_pipeline(&(sg_pipeline_desc){
            .layout = {
                .buffers[0].stride = sizeof(float) * 4,
                .attrs = {
                    [ATTR_display_vs_v_pos] = {
                        .format = SG_VERTEXFORMAT_FLOAT2,
                        .buffer_index = 0
                    },
                    [ATTR_display_vs_v_uv] = {
                        .format = SG_VERTEXFORMAT_FLOAT2,
                        .buffer_index = 0,
                        .offset = sizeof(float) * 2
                    }
                }
            },
            .shader = gfx_data->display_shader,
            .index_type = SG_INDEXTYPE_UINT16,
            .label = "window-pip"
        });
    }

    return 0;
}

void game_renderer_free(GameRenderer *gfx_data) {
    free(gfx_data->tile_buf);
    free(gfx_data->tile_palette_buf);

    free(gfx_data->sprite_buf);
    free(gfx_data->sprite_palette_buf);

    // free graphics resources
    sg_destroy_shader(gfx_data->shader);
    sg_destroy_shader(gfx_data->display_shader);
    sg_destroy_image(gfx_data->character_texture);
    sg_destroy_image(gfx_data->sprite_texture);
    sg_destroy_image(gfx_data->palette_texture);

    sg_destroy_attachments(gfx_data->fbo);
    sg_destroy_image(gfx_data->fbo_color);
    sg_destroy_image(gfx_data->fbo_depth);

    for (int i = 0; i < 3; i++)
    {
        sg_destroy_buffer(gfx_data->tile_buffers[i]);
        sg_destroy_buffer(gfx_data->sprite_buffers[i]);
    }

    for (int i = 0; i < 2; i++)
    {
        sg_destroy_buffer(gfx_data->display_vbuffers[i]);
    }

    sg_destroy_pipeline(gfx_data->game_pip);
    sg_destroy_pipeline(gfx_data->window_pip);
    sg_destroy_sampler(gfx_data->sampler);
}

static void draw_tiles(GameRenderer* rndr, DisplayInfo* display_info) {
    int tile_i = 0;
    int palette_i = 0;

    float tile_size = 8.0f / 128.0f;
    for (int x = 0; x < TILE_WIDTH; x++) {
        for (int y = 0; y < TILE_HEIGHT; y++) {
            uint8_t tile_index = display_info->tiles[x][y];
            uint8_t palette_index = display_info->tile_palettes[x][y];

            int x = tile_index % 16;
            int y = (int)(tile_index / 16);

            // update uvs
            rndr->tile_buf[tile_i + 2] = tile_size * x;
            rndr->tile_buf[tile_i + 3] = tile_size * (y+1);

            rndr->tile_buf[tile_i + 6] = tile_size * x;
            rndr->tile_buf[tile_i + 7] = tile_size * y;

            rndr->tile_buf[tile_i + 10] = tile_size * (x+1);
            rndr->tile_buf[tile_i + 11] = tile_size * y;

            rndr->tile_buf[tile_i + 14] = tile_size * (x+1);
            rndr->tile_buf[tile_i + 15] = tile_size * (y+1);

            assert(tile_i + 16 <= rndr->tile_buf_size / sizeof(float));

            // update palettes
            float fpalette = (float) palette_index;
            rndr->tile_palette_buf[palette_i++] = fpalette;
            rndr->tile_palette_buf[palette_i++] = fpalette;
            rndr->tile_palette_buf[palette_i++] = fpalette;
            rndr->tile_palette_buf[palette_i++] = fpalette;
            
            assert(palette_i <= rndr->tile_palette_buf_size / sizeof(float));
            
            tile_i += 16;
        }
    }

    // update tile buffer
    sg_update_buffer(rndr->tile_buffers[VERTEX_UV_BUFFER], &(sg_range){ .ptr = rndr->tile_buf, .size = rndr->tile_buf_size });

    // update palette buffer
    sg_update_buffer(rndr->tile_buffers[PALETTE_BUFFER], &(sg_range){ .ptr = rndr->tile_palette_buf, .size = rndr->tile_palette_buf_size});

    // switch shader to character texture
    sg_apply_bindings(&(sg_bindings){
        .fs.images[SLOT_uPalette] = rndr->palette_texture,
        .fs.images[SLOT_uTexture] = rndr->character_texture,
        .fs.samplers[SLOT_uPaletteS] = rndr->sampler,
        .fs.samplers[SLOT_uTextureS] = rndr->sampler,
        .vertex_buffers[0] = rndr->tile_buffers[VERTEX_UV_BUFFER],
        .vertex_buffers[1] = rndr->tile_buffers[PALETTE_BUFFER],
        .index_buffer = rndr->tile_buffers[INDEX_BUFFER]
    });

    sg_draw(0, TILE_WIDTH * TILE_HEIGHT * 6, 1);
}

static void draw_sprites(GameRenderer* rndr, DisplayInfo* display_info) {
    int mesh_i = 0;
    int palette_i = 0;
    int sprite_count = 0;

    float sprite_uv_size = 16.0f / 128.0f;
    for (int i = 0; i < MAX_SPRITES; i++) {
        Sprite sprite = display_info->sprites[i];

        if (sprite.flags & SPRITE_ACTIVE) {
            sprite_count++;

            int uv_x = sprite.sprite % 8;
            int uv_y = (int)(sprite.sprite / 8);

            int uvxo = (sprite.flags & SPRITE_FLIP_X) ? uv_x + 1 : uv_x;
            int uvyo = (sprite.flags & SPRITE_FLIP_Y) ? uv_y : uv_y + 1;
            float uvdx = (sprite.flags & SPRITE_FLIP_X) ? -1.0f : 1.0f;
            float uvdy = (sprite.flags & SPRITE_FLIP_Y) ? 1.0f : -1.0f;
            float locx = (float)sprite.location_x / 8.0f;
            float locy = (float)sprite.location_y / 8.0f;

            rndr->sprite_buf[mesh_i++] = locx;
            rndr->sprite_buf[mesh_i++] = locy;
            rndr->sprite_buf[mesh_i++] = uvxo * sprite_uv_size;
            rndr->sprite_buf[mesh_i++] = uvyo * sprite_uv_size;

            rndr->sprite_buf[mesh_i++] = locx;
            rndr->sprite_buf[mesh_i++] = locy + 2.0f;
            rndr->sprite_buf[mesh_i++] = uvxo * sprite_uv_size;
            rndr->sprite_buf[mesh_i++] = (uvyo+uvdy) * sprite_uv_size;

            rndr->sprite_buf[mesh_i++] = locx + 2.0f;
            rndr->sprite_buf[mesh_i++] = locy + 2.0f;
            rndr->sprite_buf[mesh_i++] = (uvxo+uvdx) * sprite_uv_size;
            rndr->sprite_buf[mesh_i++] = (uvyo+uvdy) * sprite_uv_size;

            rndr->sprite_buf[mesh_i++] = locx + 2.0f;
            rndr->sprite_buf[mesh_i++] = locy;
            rndr->sprite_buf[mesh_i++] = (uvxo+uvdx) * sprite_uv_size;
            rndr->sprite_buf[mesh_i++] = uvyo * sprite_uv_size;

            assert(mesh_i <= rndr->sprite_buf_size / sizeof(float));

            // update palettes
            float fpalette = (float) sprite.palette;
            rndr->sprite_palette_buf[palette_i++] = fpalette;
            rndr->sprite_palette_buf[palette_i++] = fpalette;
            rndr->sprite_palette_buf[palette_i++] = fpalette;
            rndr->sprite_palette_buf[palette_i++] = fpalette;

            assert(palette_i <= rndr->sprite_palette_buf_size / sizeof(float));
        }
    }

    // update sprite buffer
    sg_update_buffer(rndr->sprite_buffers[VERTEX_UV_BUFFER], &(sg_range){ .ptr = rndr->sprite_buf, .size = rndr->sprite_buf_size });

    // update palette buffer
    sg_update_buffer(rndr->sprite_buffers[PALETTE_BUFFER], &(sg_range){ .ptr = rndr->sprite_palette_buf, .size = rndr->sprite_palette_buf_size });

    if (sprite_count > 0)
    {
        // switch shader to sprite texture
        sg_apply_bindings(&(sg_bindings){
            .fs.images[SLOT_uPalette] = rndr->palette_texture,
            .fs.images[SLOT_uTexture] = rndr->sprite_texture,
            .fs.samplers[SLOT_uPaletteS] = rndr->sampler,
            .fs.samplers[SLOT_uTextureS] = rndr->sampler,
            .vertex_buffers[0] = rndr->sprite_buffers[VERTEX_UV_BUFFER],
            .vertex_buffers[1] = rndr->sprite_buffers[PALETTE_BUFFER],
            .index_buffer = rndr->sprite_buffers[INDEX_BUFFER],
        });

        sg_draw(0, sprite_count * 6, 1);
    }
}

void game_renderer_draw(GameRenderer* rndr, DisplayInfo* display_info) {
    int screen_width = display_info->screen_width + SCREEN_PADDING_X * 2;
    int screen_height = display_info->screen_height + SCREEN_PADDING_Y * 2;

    bool origin_top_left = sg_query_backend() == SG_BACKEND_D3D11;

    // render game into framebuffer
    {
        sg_begin_pass(&(sg_pass){
            .attachments = rndr->fbo,
            .action = {
                .colors[0] = {
                    .load_action = SG_LOADACTION_CLEAR,
                    .clear_value = { 0.0f, 0.0f, 0.0f, 1.0f }
                }
            },
            .label = "game-pass"
        });

        sg_apply_pipeline(rndr->game_pip);
        sg_apply_viewport(0, 0, screen_width, screen_height, origin_top_left);
        sg_apply_scissor_rect(0, 0, screen_width, screen_height, origin_top_left);

        float scale_x = 2.0f / screen_width * 8.0f;
        float scale_y = 2.0f / screen_height * 8.0f;
        float matrix[16];
        
        if (!origin_top_left)
        {
            float v[16] = {
                scale_x,    0.0,    0.0,    0.0,
                0.0,    -scale_y,    0.0,    0.0,
                0.0,    0.0,    1.0,    0.0,
                -1.0 + SCREEN_PADDING_X / 8.0f * scale_x,   1.0 - SCREEN_PADDING_Y / 8.0f * scale_y,    0.0,    1.0,
            };
            memcpy(matrix, v, sizeof(v));
        }
        else
        {
            float v[16] = {
                scale_x,    0.0,    0.0,    0.0,
                0.0,    scale_y,    0.0,    0.0,
                0.0,    0.0,    1.0,    0.0,
                -1.0 + SCREEN_PADDING_X / 8.0f * scale_x,   -1.0 + SCREEN_PADDING_Y / 8.0f * scale_y,    0.0,    1.0,
            };
            memcpy(matrix, v, sizeof(v));
        }

        vs_params_t vs_params;
        memcpy(&vs_params.mvp, matrix, sizeof(matrix));
        sg_apply_uniforms(SG_SHADERSTAGE_VS, SLOT_vs_params, &SG_RANGE(vs_params));

        draw_tiles(rndr, display_info);
        draw_sprites(rndr, display_info);

        sg_end_pass();
    }

    // render display into window
    {
        sg_begin_pass(&(sg_pass){
            .label = "main-pass",
            .swapchain = sglue_swapchain(),
            .action = {
                .colors[0] = {
                    .load_action = SG_LOADACTION_CLEAR,
                    .clear_value = { 0.0f, 0.0f, 0.0f, 1.0f }
                },
            }
        });

        sg_apply_pipeline(rndr->window_pip);

        // update matrix transform
        float scale_x = 2.0f / rndr->view_width;
        float scale_y = 2.0f / rndr->view_height;
        //float scale_x = 1.0f / TILE_WIDTH * ((float)rndr->view_width / interface->display_info.screen_width);

        float x = (rndr->view_width - screen_width * 2.0f) / 2.0f;
        float y = (rndr->view_height - screen_height * 2.0f) / 2.0f;

        float matrix[16] = {
            scale_x,    0.0,    0.0,    0.0,
            0.0,    -scale_y,    0.0,    0.0,
            0.0,    0.0,    1.0,    0.0,
            -1.0 + x * scale_x,    1.0 + - y * scale_y,    0.0,    1.0,
        };

        // update uniforms
        vs_params_t vs_params;
        memcpy(&vs_params.mvp, matrix, sizeof(matrix));
        screen_size_t screen_size = {
            .uScreenWidth = screen_width,
            .uScreenHeight = screen_height
        };

        sg_apply_uniforms(SG_SHADERSTAGE_VS, SLOT_vs_params, &SG_RANGE(vs_params));
        sg_apply_uniforms(SG_SHADERSTAGE_FS, SLOT_screen_size, &SG_RANGE(screen_size));

        // bindings and draw
        sg_apply_bindings(&(sg_bindings){
            .fs.images[SLOT_uTexture] = rndr->fbo_color,
            .fs.samplers[SLOT_uTextureS] = rndr->sampler,
            .vertex_buffers[0] = rndr->display_vbuffers[0],
            .index_buffer = rndr->display_vbuffers[1],
        });

        sg_draw(0, 6, 1);
        sg_end_pass();
    }

    sg_commit();
}